﻿using OperationPlugins.Applications.Models;
using OperationPlugins.Applications.Remoting;
using OperationPlugins.Compositions;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Policy;

namespace OperationPlugins.Applications
{
    [Serializable]
    internal sealed class Sandbox
    {
        internal AppInstance AppInstance { get; private set; }

        internal Assembly HostAssembly { get; private set; }

        internal string FriendlyName { get; private set; }

        internal DirectoryInfo Directory { get; private set; }

        internal FileInfo RunnerAssemblyFile { get; private set; }

        internal AppDomain Domain { get; private set; }

        internal SandboxAssemblyResolver AssemblyResolver { get; private set; }

        internal ISandboxRunner Runner { get; private set; }

        /// <summary>
        /// No public constructor available!
        /// </summary>
        private Sandbox()
        {            
        }

        /// <summary>
        /// Creates a new sandbox for the specified application.
        /// </summary>
        /// <param name="appInstance">The application definition.</param>
        /// <param name="hostAssembly">The host assembly.</param>
        /// <returns>Sandbox.</returns>
        internal static Sandbox CreateSandbox(AppInstance appInstance, Assembly hostAssembly)
        {
            if (appInstance == null)
            {
                throw new ArgumentNullException("appInstance");
            }

            Sandbox sandbox = new Sandbox { AppInstance = appInstance, HostAssembly = hostAssembly };
            sandbox.Initialize();            
            return sandbox;
        }

        /// <summary>
        /// Initializes the sandbox.
        /// </summary>
        private void Initialize()
        {
            InitializeEnvironment();
            InitializeAppDomain();
            InitializeRunner();
        }

        /// <summary>
        /// Ininitializes the environment.
        /// </summary>
        private void InitializeEnvironment()
        {
            FriendlyName = string.Format("{0}.{1}", AppInstance.Name, Guid.NewGuid());

            DirectoryInfo tmp = new DirectoryInfo(Path.GetTempPath());
            DirectoryInfo app = tmp.CreateSubdirectory("OperationPlugins");
            DirectoryInfo snd = app.CreateSubdirectory(Guid.NewGuid().ToString());

            Directory = snd.CreateSubdirectory(Guid.NewGuid().ToString());
            RunnerAssemblyFile = new FileInfo(string.Format(@"{0}\{1}.dll", Directory.FullName, Guid.NewGuid()));
        }

        /// <summary>
        /// Initializes the new app-domain.
        /// </summary>
        private void InitializeAppDomain()
        {
            // Get a reference to the default application domain. 
            AppDomain current = AppDomain.CurrentDomain;

            // Create the AppDomainSetup that will be used to set up the child AppDomain.    
            AppDomainSetup setup = current.SetupInformation;
            setup.ApplicationBase = current.BaseDirectory;
            setup.ApplicationTrust = current.ApplicationTrust;
            setup.ApplicationName = AppInstance.Name;
            //ads.ConfigurationFile = 
           
            // Use the evidence from the default application domain to create evidence for the child application domain. 
            Evidence ev = new Evidence(current.Evidence);

            Domain = AppDomain.CreateDomain(FriendlyName, ev, setup);

            AssemblyResolver = new SandboxAssemblyResolver(this);
            AssemblyResolver.Initialize();
        }

        /// <summary>
        /// Initializes the sandbox runner.
        /// </summary>
        private void InitializeRunner()
        {
            // Get the C# code.
            string code, @namespace, @class;
            code = GetCSharpCode(out @namespace, out @class);

            // Resolve referenced assemblies.
            string[] referencedAssemblies = AssemblyResolver.GetReferencedAssemblies();

            // Set up compiler parameters.
            CompilerParameters parameters = new CompilerParameters();
            parameters.GenerateInMemory = false;
            parameters.TreatWarningsAsErrors = true;
            parameters.GenerateExecutable = false;
            parameters.IncludeDebugInformation = false;
            parameters.OutputAssembly = RunnerAssemblyFile.FullName;
            parameters.TempFiles = new TempFileCollection(Directory.FullName, false);
            parameters.ReferencedAssemblies.AddRange(referencedAssemblies);
            
            // Get the right compiler.
            CodeDomProvider compiler = CodeDomProvider.CreateProvider("CSharp");

            // Compile the dynamic code, and throw an exception if there were any compiler errors.          
            CompilerResults compiled = compiler.CompileAssemblyFromSource(parameters, code);
            if (compiled.Errors.HasErrors)
            {
                throw new SandboxException(compiled.Errors, AppInstance);
            }

            try
            {
                // Create a new factory class.
                SandboxRunnerFactory factory = (SandboxRunnerFactory)Domain.CreateInstanceAndUnwrap(typeof(SandboxRunnerFactory).Assembly.FullName, typeof(SandboxRunnerFactory).FullName);

                // With the help of this factory, we can now create a real "LiveClass" instance.
                object instance = factory.CreateRemoteScript(RunnerAssemblyFile.FullName,
                                                             string.Format("{0}.{1}", @namespace, @class),
                                                             null);

                // ...and cast the object to the remote interface to avoid loading type info.
                Runner = (ISandboxRunner)instance;
            }
            catch (Exception e)
            {
                throw new SandboxException(string.Format("Failed to initialize a sandbox runner for application {0}", AppInstance.AppId), e);
            }
        }

        /// <summary>
        /// Gets the C# code of the sandbox runner.
        /// </summary>
        /// <param name="namespace">The namespace.</param>
        /// <param name="class">The class name.</param>
        /// <returns>C# code.</returns>
        private static string GetCSharpCode(out string @namespace, out string @class)
        {
            string code;
            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("OperationPlugins.Applications.Remoting.CSharp.template"))
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    code = reader.ReadToEnd();
                }
            }

            @namespace = string.Format("OperationPlugins.Applications.Remoting.Proxies._{0}", Guid.NewGuid().ToString().Replace("-", string.Empty));
            @class = string.Format("SandboxRunnerProxy_{0}", Guid.NewGuid().ToString().Replace("-", string.Empty));

            code = code.Replace("$NAMESPACE$", @namespace);
            code = code.Replace("$CLASS$", @class);

            return code;
        }



        /// <summary>
        /// Loads referenced assemblies when the the resulution of assemblies fails.
        /// </summary>
        [Serializable]
        internal sealed class SandboxAssemblyResolver
        {
            private readonly Dictionary<string, Assembly> _assemblies = new Dictionary<string, Assembly>();

            private Sandbox Sandbox { get; set; }

            internal SandboxAssemblyResolver(Sandbox sandbox)
            {
                if (sandbox == null)
                {
                    throw new ArgumentNullException("sandbox");
                }

                Sandbox = sandbox;
            }

            internal void Initialize()
            {
                Sandbox.Domain.SetData("SandboxAssemblyResolver", this);
                Sandbox.Domain.AssemblyResolve += Domain_AssemblyResolve;
            }

            private Assembly Domain_AssemblyResolve(object sender, ResolveEventArgs args)
            {
                Debugger.Break();
                return null;
            }

            internal string[] GetReferencedAssemblies()
            {
                List<string> files = new List<string>();
                GetReferencedAssemblies(Sandbox.HostAssembly, files);
                return files.ToArray();
            }

            private static void GetReferencedAssemblies(Assembly assembly, List<string> files)
            {
                AssemblyName[] referencedAssemblies = assembly.GetReferencedAssemblies();

                foreach (AssemblyName referencedAssembly in referencedAssemblies)
                {
                    Assembly tmp = Assembly.Load(referencedAssembly);

                    string file = tmp.CodeBase.Replace(@"file:///", string.Empty);

                    if (files.Contains(file) == false)
                    {
                        files.Add(file);

                        GetReferencedAssemblies(tmp, files);
                    }
                }
            }


        }


    }
}